<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>

                    <h4>SMTP发送邮件</h4>
                    <div class="x-wiki-info"><span>725次阅读</span></div>
                    <hr style="border-top-color:#ccc" />
                    <div class="x-wiki-content x-content"><p>SMTP是发送邮件的协议，Python内置对SMTP的支持，可以发送纯文本邮件、HTML邮件以及带附件的邮件。</p>
<p>Python对SMTP支持有<code>smtplib</code>和<code>email</code>两个模块，<code>email</code>负责构造邮件，<code>smtplib</code>负责发送邮件。</p>
<p>首先，我们来构造一个最简单的纯文本邮件：</p>
<pre><code>from email.mime.text import MIMEText
msg = MIMEText(&#39;hello, send by Python...&#39;, &#39;plain&#39;, &#39;utf-8&#39;)
</code></pre><p>注意到构造<code>MIMEText</code>对象时，第一个参数就是邮件正文，第二个参数是MIME的subtype，传入<code>&#39;plain&#39;</code>，最终的MIME就是<code>&#39;text/plain&#39;</code>，最后一定要用<code>utf-8</code>编码保证多语言兼容性。</p>
<p>然后，通过SMTP发出去：</p>
<pre><code># 输入Email地址和口令:
from_addr = raw_input(&#39;From: &#39;)
password = raw_input(&#39;Password: &#39;)
# 输入SMTP服务器地址:
smtp_server = raw_input(&#39;SMTP server: &#39;)
# 输入收件人地址:
to_addr = raw_input(&#39;To: &#39;)

import smtplib
server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
</code></pre><p>我们用<code>set_debuglevel(1)</code>就可以打印出和SMTP服务器交互的所有信息。SMTP协议就是简单的文本命令和响应。<code>login()</code>方法用来登录SMTP服务器，<code>sendmail()</code>方法就是发邮件，由于可以一次发给多个人，所以传入一个<code>list</code>，邮件正文是一个<code>str</code>，<code>as_string()</code>把<code>MIMEText</code>对象变成<code>str</code>。</p>
<p>如果一切顺利，就可以在收件人信箱中收到我们刚发送的Email：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/0014079993639301a4130bf23574d3586f91928c4f6d6e3000" alt="send-mail"></p>
<p>仔细观察，发现如下问题：</p>
<ol>
<li>邮件没有主题；</li>
<li>收件人的名字没有显示为友好的名字，比如<code>Mr Green &lt;green@example.com&gt;</code>；</li>
<li>明明收到了邮件，却提示不在收件人中。</li>
</ol>
<p>这是因为邮件主题、如何显示发件人、收件人等信息并不是通过SMTP协议发给MTA，而是包含在发给MTA的文本中的，所以，我们必须把<code>From</code>、<code>To</code>和<code>Subject</code>添加到<code>MIMEText</code>中，才是一封完整的邮件：</p>
<pre><code># -*- coding: utf-8 -*-

from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib

def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr(( \
        Header(name, &#39;utf-8&#39;).encode(), \
        addr.encode(&#39;utf-8&#39;) if isinstance(addr, unicode) else addr))

from_addr = raw_input(&#39;From: &#39;)
password = raw_input(&#39;Password: &#39;)
to_addr = raw_input(&#39;To: &#39;)
smtp_server = raw_input(&#39;SMTP server: &#39;)

msg = MIMEText(&#39;hello, send by Python...&#39;, &#39;plain&#39;, &#39;utf-8&#39;)
msg[&#39;From&#39;] = _format_addr(u&#39;Python爱好者 &lt;%s&gt;&#39; % from_addr)
msg[&#39;To&#39;] = _format_addr(u&#39;管理员 &lt;%s&gt;&#39; % to_addr)
msg[&#39;Subject&#39;] = Header(u&#39;来自SMTP的问候……&#39;, &#39;utf-8&#39;).encode()

server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
</code></pre><p>我们编写了一个函数<code>_format_addr()</code>来格式化一个邮件地址。注意不能简单地传入<code>name &lt;addr@example.com&gt;</code>，因为如果包含中文，需要通过<code>Header</code>对象进行编码。</p>
<p><code>msg[&#39;To&#39;]</code>接收的是字符串而不是list，如果有多个邮件地址，用<code>,</code>分隔即可。</p>
<p>再发送一遍邮件，就可以在收件人邮箱中看到正确的标题、发件人和收件人：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/00140800242227674f8f0eed1a64b9e95f2ab9752755e23000" alt="mail-with-header"></p>
<p>你看到的收件人的名字很可能不是我们传入的<code>管理员</code>，因为很多邮件服务商在显示邮件时，会把收件人名字自动替换为用户注册的名字，但是其他收件人名字的显示不受影响。</p>
<p>如果我们查看Email的原始内容，可以看到如下经过编码的邮件头：</p>
<pre><code>From: =?utf-8?b?UHl0aG9u54ix5aW96ICF?= &lt;xxxxxx@163.com&gt;
To: =?utf-8?b?566h55CG5ZGY?= &lt;xxxxxx@qq.com&gt;
Subject: =?utf-8?b?5p2l6IeqU01UUOeahOmXruWAmeKApuKApg==?=
</code></pre><p>这就是经过<code>Header</code>对象编码的文本，包含utf-8编码信息和Base64编码的文本。如果我们自己来手动构造这样的编码文本，显然比较复杂。</p>
<h3 id="-html-">发送HTML邮件</h3>
<p>如果我们要发送HTML邮件，而不是普通的纯文本文件怎么办？方法很简单，在构造<code>MIMEText</code>对象时，把HTML字符串传进去，再把第二个参数由<code>plain</code>变为<code>html</code>就可以了：</p>
<pre><code>msg = MIMEText(&#39;&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello&lt;/h1&gt;&#39; +
    &#39;&lt;p&gt;send by &lt;a href=&quot;http://www.python.org&quot;&gt;Python&lt;/a&gt;...&lt;/p&gt;&#39; +
    &#39;&lt;/body&gt;&lt;/html&gt;&#39;, &#39;html&#39;, &#39;utf-8&#39;)
</code></pre><p>再发送一遍邮件，你将看到以HTML显示的邮件：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/001408003582561ea3bdf0296fe49e29a7c20f52ded48a1000" alt="html-mail"></p>
<h3 id="-">发送附件</h3>
<p>如果Email中要加上附件怎么办？带附件的邮件可以看做包含若干部分的邮件：文本和各个附件本身，所以，可以构造一个<code>MIMEMultipart</code>对象代表邮件本身，然后往里面加上一个<code>MIMEText</code>作为邮件正文，再继续往里面加上表示附件的<code>MIMEBase</code>对象即可：</p>
<pre><code># 邮件对象:
msg = MIMEMultipart()
msg[&#39;From&#39;] = _format_addr(u&#39;Python爱好者 &lt;%s&gt;&#39; % from_addr)
msg[&#39;To&#39;] = _format_addr(u&#39;管理员 &lt;%s&gt;&#39; % to_addr)
msg[&#39;Subject&#39;] = Header(u&#39;来自SMTP的问候……&#39;, &#39;utf-8&#39;).encode()

# 邮件正文是MIMEText:
msg.attach(MIMEText(&#39;send with file...&#39;, &#39;plain&#39;, &#39;utf-8&#39;))

# 添加附件就是加上一个MIMEBase，从本地读取一个图片:
with open(&#39;/Users/michael/Downloads/test.png&#39;, &#39;rb&#39;) as f:
    # 设置附件的MIME和文件名，这里是png类型:
    mime = MIMEBase(&#39;image&#39;, &#39;png&#39;, filename=&#39;test.png&#39;)
    # 加上必要的头信息:
    mime.add_header(&#39;Content-Disposition&#39;, &#39;attachment&#39;, filename=&#39;test.png&#39;)
    mime.add_header(&#39;Content-ID&#39;, &#39;&lt;0&gt;&#39;)
    mime.add_header(&#39;X-Attachment-Id&#39;, &#39;0&#39;)
    # 把附件的内容读进来:
    mime.set_payload(f.read())
    # 用Base64编码:
    encoders.encode_base64(mime)
    # 添加到MIMEMultipart:
    msg.attach(mime)
</code></pre><p>然后，按正常发送流程把<code>msg</code>（注意类型已变为<code>MIMEMultipart</code>）发送出去，就可以收到如下带附件的邮件：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/0014080077329276557d648f58540f48d04e58520504665000" alt="mimemultipart"></p>
<h3 id="-">发送图片</h3>
<p>如果要把一个图片嵌入到邮件正文中怎么做？直接在HTML邮件中链接图片地址行不行？答案是，大部分邮件服务商都会自动屏蔽带有外链的图片，因为不知道这些链接是否指向恶意网站。</p>
<p>要把图片嵌入到邮件正文中，我们只需按照发送附件的方式，先把邮件作为附件添加进去，然后，在HTML中通过引用<code>src=&quot;cid:0&quot;</code>就可以把附件作为图片嵌入了。如果有多个图片，给它们依次编号，然后引用不同的<code>cid:x</code>即可。</p>
<p>把上面代码加入<code>MIMEMultipart</code>的<code>MIMEText</code>从<code>plain</code>改为<code>html</code>，然后在适当的位置引用图片：</p>
<pre><code>msg.attach(MIMEText(&#39;&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello&lt;/h1&gt;&#39; +
    &#39;&lt;p&gt;&lt;img src=&quot;cid:0&quot;&gt;&lt;/p&gt;&#39; +
    &#39;&lt;/body&gt;&lt;/html&gt;&#39;, &#39;html&#39;, &#39;utf-8&#39;))
</code></pre><p>再次发送，就可以看到图片直接嵌入到邮件正文的效果：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/001408019030110a0be121000cc46139f7a72982b19daf3000" alt="email-inline-image"></p>
<h3 id="-html-plain-">同时支持HTML和Plain格式</h3>
<p>如果我们发送HTML邮件，收件人通过浏览器或者Outlook之类的软件是可以正常浏览邮件内容的，但是，如果收件人使用的设备太古老，查看不了HTML邮件怎么办？</p>
<p>办法是在发送HTML的同时再附加一个纯文本，如果收件人无法查看HTML格式的邮件，就可以自动降级查看纯文本邮件。</p>
<p>利用<code>MIMEMultipart</code>就可以组合一个HTML和Plain，要注意指定subtype是<code>alternative</code>：</p>
<pre><code>msg = MIMEMultipart(&#39;alternative&#39;)
msg[&#39;From&#39;] = ...
msg[&#39;To&#39;] = ...
msg[&#39;Subject&#39;] = ...

msg.attach(MIMEText(&#39;hello&#39;, &#39;plain&#39;, &#39;utf-8&#39;))
msg.attach(MIMEText(&#39;&lt;html&gt;&lt;body&gt;&lt;h1&gt;Hello&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;&#39;, &#39;html&#39;, &#39;utf-8&#39;))
# 正常发送msg对象...
</code></pre><h3 id="-smtp">加密SMTP</h3>
<p>使用标准的25端口连接SMTP服务器时，使用的是明文传输，发送邮件的整个过程可能会被窃听。要更安全地发送邮件，可以加密SMTP会话，实际上就是先创建SSL安全连接，然后再使用SMTP协议发送邮件。</p>
<p>某些邮件服务商，例如Gmail，提供的SMTP服务必须要加密传输。我们来看看如何通过Gmail提供的安全SMTP发送邮件。</p>
<p>必须知道，Gmail的SMTP端口是587，因此，修改代码如下：</p>
<pre><code>smtp_server = &#39;smtp.gmail.com&#39;
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
server.set_debuglevel(1)
...
</code></pre><p>只需要在创建<code>SMTP</code>对象后，立刻调用<code>starttls()</code>方法，就创建了安全连接。后面的代码和前面的发送邮件代码完全一样。</p>
<p>如果因为网络问题无法连接Gmail的SMTP服务器，请相信我们的代码是没有问题的，你需要对你的网络设置做必要的调整。</p>
<h3 id="-">小结</h3>
<p>使用Python的smtplib发送邮件十分简单，只要掌握了各种邮件类型的构造方法，正确设置好邮件头，就可以顺利发出。</p>
<p>构造一个邮件对象就是一个<code>Messag</code>对象，如果构造一个<code>MIMEText</code>对象，就表示一个文本邮件对象，如果构造一个<code>MIMEImage</code>对象，就表示一个作为附件的图片，要把多个对象组合起来，就用<code>MIMEMultipart</code>对象，而<code>MIMEBase</code>可以表示任何对象。它们的继承关系如下：</p>
<pre><code>Message
+- MIMEBase
   +- MIMEMultipart
   +- MIMENonMultipart
      +- MIMEMessage
      +- MIMEText
      +- MIMEImage
</code></pre><p>这种嵌套关系就可以构造出任意复杂的邮件。你可以通过<a href="https://docs.python.org/2/library/email.mime.html">email.mime文档</a>查看它们所在的包以及详细的用法。</p>
<p>源码参考：</p>
<p><a href="https://github.com/michaelliao/learn-python/tree/master/email">https://github.com/michaelliao/learn-python/tree/master/email</a></p>
</div>

                    <hr style="border-top-color:#ccc" />

                    